#!/usr/bin/python2.4

# pylint: disable-msg=C6304

"""Reads HTML from the xtide server and extracts a tide table.

The HTML returned by the xtide server is a number of non nesting tables.
The tables are not well formed, and don't follow good HTML practice.

When I first wrote this code, it was reasonably clean, since I wrote
it to parse the saved output from Chrome.  cleans up the HTML, or the
xtide server is sensitive to the browser type?)

The main rows in the table are extracted and stored in an intermediate
data structure.  The latlon data and the nominal ebb/flood current direction
are extracted as well; these do not necessarily come before the main
rows in the table.

Once all the data is extracted, the intermediate data structure
is updated with the latlon and current direction.
"""

import HTMLParser
import re
import time
import urllib2

from matrix import LatLon
from matrix import LatLonDeg
from matrix import Polar
import timeformat
import vectorfield


class XtideTableParser(HTMLParser.HTMLParser):
  """Attaches to the xtide server and extracts the Tidal Current vectors."""

  def __init__(self, year, vf, attribution='xtide'):
    HTMLParser.HTMLParser.__init__(self)
    self._year = year
    self._vf = vf
    self._attribution = attribution

    self._indent = 0
    self._table = None
    self._row = None
    self._grab_text = False
    self._flood_value = None  # scraped from table
    self._ebb_value = None  # scraped from table
    self._latlon = None  # scraped from table
    self._conditions = []

  def handle_starttag(self, tag, unused_attributes):
    """Override method called on the start of a tag."""
    if tag == 'table':
      self._indent += 1

    elif tag == 'tbody':
      if self._table:
        self.ConsumeTableInHTML(self._table)
      self._table = []

    elif tag == 'tr':
      if Trace():
        print 'start tr {'
      self._row = []

    elif tag == 'td':
      if Trace():
        print 'start td {'
      self._grab_text = True
      self._td_string = ''

  def handle_data(self, data):
    """Override method called when presented with data."""
    if Trace():
      print 'XXX %s consume data %s' % (self._grab_text, data,)
    if self._grab_text:
      self._td_string += ' ' + data

  def handle_endtag(self, tag):
    """Override method called when presented with the end tag."""
    if tag == 'table':
      self._indent -= 1

    elif tag == 'tbody':
      self.ConsumeTableInHTML(self._table)
      self._table = []

    elif tag == 'tr':
      if Trace():
        print 'end tr } with %s from %s' % (self._row, self._table)
      naked_row = False
      if self._table is None:
        self._table = []
        naked_row = True
      self._table.append(self._row)  # Done growing the last row, if any
      self._row = []
      if naked_row:
        if Trace():
          print 'XXX naked_row %s' % (self._table,)
        self.ConsumeTableInHTML(self._table)
        self._table = []

    elif tag == 'td':
      if Trace():
        print 'end td }'
      if self._td_string:
        self._row.append(self._td_string.strip())
      self._td_string = ''
      self._grab_text = False

  def ConsumeTableInHTML(self, table):
    """Apply heuristics to extract the 3 blocks of data we need."""
    if not table:
      return
    self.ExtractCurrentAngleValues(table)
    self.ExtractLatLon(table)
    self.ExtractTidalValues(table)

  def RegisterTidalValue(self, date_str, time_str, flow_value):
    """Save a scraped tidal value as a partially filled out CoordsToVector."""
    unix_epoch_secs = timeformat.ScanXtideTimeString(
        ' '.join([self._year, date_str, time_str]))
    m = re.match(r'([-.0-9]+)([EFS])', flow_value)
    value = float(m.group(1))
    is_ebb = m.group(2) == 'E'
    if Trace():
      print 'RegisterTidalValue ==> %d ==> %s %4.1f E=%s' % (
          unix_epoch_secs,
          timeformat.FormatTime(unix_epoch_secs),
          value,
          is_ebb)
    condition = vectorfield.CoordsToVector(
        unix_epoch_secs,
        LatLon(0, 0),  # will be patched in later with proper latlon
        Polar(value, 0),  # will be patched in later with proper angle
        attribution=self._attribution)
    self._conditions.append(condition)

  def ExtractCurrentAngleValues(self, table):
    """Comb through table to see if it contains the Ebb/Float info."""
    for row in table:
      is_direction_row = False
      for field in row:
        is_direction_row |= 'Nominal current' in str(field)
      if is_direction_row:
        # print 'XXX direction_row %s' % (row,)
        next_is_flood = False
        next_is_ebb = False
        for field in row:
          if 'Flood' in field:
            next_is_flood = True
          elif 'Ebb' in field:
            next_is_ebb = True
          elif next_is_flood:
            next_is_flood = False
            self._flood_value = float(re.sub(r'[,]', '', field))
          elif next_is_ebb:
            next_is_ebb = False
            self._ebb_value = float(re.sub(r'[,]', '', field))

        if self._ebb_value is None:
          row = re.sub(r'\\n', ' ', str(row))
          row = re.sub(r'[, \W]+', ' ', str(row))
          m = re.search(r'Flood ([0-9]+).*Ebb ([0-9]+)', row)
          if m:
            self._flood_value = float(m.group(1))
            self._ebb_value = float(m.group(2))
            if Trace():
              print 'flood=%g ebb=%g' % (self._flood_value, self._ebb_value)

  def ExtractLatLon(self, table):
    """Comb through table to see if it contains the LatLon info."""
    for row in table:
      for field in row:
        # print 'XXX ExtractLatLon on %s' % (field,)
        degree = r'(&#176;|\u00b0| )'
        m0 = re.search(u'([0-9]+)' + degree + r'([0-9.]+)\'([NS])', field)
        m1 = re.search(u'([0-9]+)' + degree + r'([0-9.]+)\'([EW])', field)
        if m0 and m1:
          lat = float(m0.group(1)) + float(m0.group(3))/60.0
          lon = float(m1.group(1)) + float(m1.group(3))/60.0
          if m0.group(4) == 'S':
            lat = -lat
          if m1.group(4) == 'W':
            lon = -lon
          self._latlon = LatLonDeg(lat, lon)
          # print 'XXX ExtractLatLon HIT on %s' % (self._latlon,)

  def ExtractTidalValues(self, table):
    """Comb through table to see if it contains tidal values."""
    if 'Time Zone' not in str(table[0]):
      return
    date_str = None
    for row in table[1:]:
      if not row:
        continue
      if re.match(r'[A-Z]', row[0]):  # putative start of month name
        date_str = row[0]
        row = row[1:]
      for i in xrange(0, len(row)):
        if re.match(r'[0-9]{4}', row[i]):
          time_value = row[i]
          if i < len(row)-1 and re.search(r'[.EF]', row[i+1]):
            self.RegisterTidalValue(date_str, time_value, row[i+1])
          else:
            self.RegisterTidalValue(date_str, time_value, '0.0S')

  def FinishUp(self, output_fd):
    """Finish the extraction.  Complete the table entries, and save."""
    if self._table:
      self.ConsumeTableInHTML(self._table)
      self._table = []

    for condition in self._conditions:
      condition.SetLatLon(self._latlon)
      p = condition.GetValue()
      if p.Magnitude() < 0:
        angle = self._ebb_value
      else:
        angle = self._flood_value
      p = Polar(abs(p.Magnitude()), float(angle))
      condition.SetValue(p)
      print >>output_fd, condition.CSVFormat()


def Trace():
  return False


def ReadTidalDataFromXtide(location='rac',
                           desired='today',
                           output_file_name='junk.csv'):
  """Read Tidal Current data from the xtide server, writes csv file.

  Args:
    location: Xtide abbreviation for tidal station, either 'rac' or 'jdf'.
    ouutput_file_name: The name of an output file to write holding CSV.
  """
  unix_time_secs = None
  if 'today' in desired or 'now' in desired:
    unix_time_secs = time.localtime(time.time())

  if unix_time_secs:
    time_suffix = time.strftime('%Y-%m', unix_time_secs)
    url_header = 'http://www.dairiki.org/tides/monthly.php'
    url_suffix = location + '/' + time_suffix
    full_url = url_header + '/' + url_suffix
    year_as_string = time.strftime('%Y', unix_time_secs)
    attribution = 'xtide_%s' % (location,)
    vf = vectorfield.VectorField('xtide_%s' % (attribution,))
    f = urllib2.urlopen(full_url)
    p = XtideTableParser(year_as_string, vf, attribution=attribution)
    while True:
      data = f.read(64 * 8192)
      if not data:
        break
      if Trace():
        print 'Feed %s' % (data,)
      p.feed(data)
    p.close()
    f.close()

    print 'Writing contents of %s to %s' % (full_url, output_file_name)
    output_fd = open(output_file_name, 'w')
    p.FinishUp(output_fd)
    output_fd.close()


def main():
  for location in ['rac', 'jdf']:
    ReadTidalDataFromXtide(location=location,
                           desired='today',
                           output_file_name='xtide_%s.csv' % (location,))


if __name__ == '__main__':
  main()
